<?php

vendor('sql');
vendor('exceptions');

function adsfadsf($v)
{
    return mb_ereg_replace('(_|%|\\\\|\'|\")', '\\\\1', $v);
}

class AppModel extends Model
{

    public $actsAs = array('transaction');
	protected $__systemQuery = false;
	protected $__models = array();

	public function &getModel($name)
	{
	    if (! isset($this->__models[$name])) {
    	    if (! class_exists($name)) {
        	    loadModel($name);
    	    }
    	    $this->__models[$name] = new $name();
	    }
        return $this->__models[$name];
	}

	public function getWords($string)
	{
	    return array_filter(mb_split("\\W+", $string), 'mb_strlen');
	}

	public function searcheCond($string, $fields, $params = array())
	{
	    $entirely  = !empty($params['entirely']);  // Фраза целиком
	    $all       = !empty($params['all']);       // Все слова
	    $sequence  = !empty($params['sequence']);  // Учитывать порядок

        $searche_words = $entirely ? array($string) : $this->getWords($string);
        array_walk($searche_words, array($this, 'escape_sql_like'));
        $key = 'or';
	    $expressions = array();
        if ($sequence) {
            $expressions = array("'%".implode('%', $searche_words)."%'");
        } else {
            if ($all && !$entirely) {
                $key = 'and';
            }
            for ($i = 0; $i < count($searche_words); $i++) {
                $expressions[] = "'%{$searche_words[$i]}%'";
            }
        }

        $result = array();
        foreach ($fields as $field) {
            $_conds = array();

            if ($key == 'or') {
                $_conds =& $result;
            } else {
                $result[][$key] = array();
                $_conds =& $result[count($result) - 1][$key];
            }

            foreach ($expressions as $expression) {
                $_conds[] = "{$field} LIKE {$expression}";
            }

            unset($_conds);
        }
        if (count($result) > 1) {
            $result = array('or' => $result);
        }

        return $result;
	}

	public function escape_sql_like($string)
	{
		return mb_ereg_replace('(_|%|\\\\|\'|")', '\\\\1', $string);
	}

	public function escape_regexp($string)
	{
		return mb_ereg_replace('(\[|\]|[.\+*?^$(){}=!<>|:])', '\\\\1', $string);
	}

	public function hightlightSearche(&$data, $string, $fields, $params = array())
	{
	    $entirely  = !empty($params['entirely']);  // Фраза целиком
	    $all       = !empty($params['all']);       // Все слова
	    $sequence  = !empty($params['sequence']);  // Учитывать порядок

        $searche_words = $entirely ? array($string) : $this->getWords($string);
        array_walk($searche_words, array($this, 'escape_regexp'));
        $key = 'or';

	    $expressions = array();
	    $replaces = array();
        if ($sequence) {
            $expressions = array("^(.*)(".implode(')(.*)(', $searche_words).")(.*)$");
            $replaces = array("^(.*)(".implode(')(.*)(', $searche_words).")(.*)$");
        } else {
            if ($all && !$entirely) {
                $key = 'and';
            }
            for ($i = 0; $i < count($searche_words); $i++) {
                $expressions[] = "^(.*)({$searche_words[$i]})(.*)$";
                $replaces[] = "\\1<strong class=\"hl\">\\2</strong>\\3";
            }
        }

        $result = array();
        foreach ($fields as $field) {
            $_conds = array();

            if ($key == 'or') {
                $_conds =& $result;
            } else {
                $result[][$key] = array();
                $_conds =& $result[count($result) - 1][$key];
            }

            foreach ($expressions as $i => $expression) {
                $data[$field] = str($data[$field])->eregiReplace($expression, $replaces[$i])->value;
            }

            unset($_conds);
        }
        if (count($result) > 1) {
            $result = array('or' => $result);
        }

        return $result;
	}

	//////// RELATIONS /////////////////////////////////////////////////////////

    public $__index_rel = array();

	function rel($relations = '')
	{
	    $rel_o = SQL_Relation($relations);

	    $tables_index = array($this->name => array('obligatory' => true, 'many' => true));
        $this->__setRel($rel_o, $tables_index, $this->name);

        $rel_o->index = $tables_index;

        return $rel_o;
	}

	function __saveRel()
	{
	    $this->__backAssociation = array();
	    foreach ($this->__associations as $type) {
	        $this->__backAssociation[$type] = $this->$type;
	        $this->$type = array();
	    }
	}

	function __indexedRel()
	{
	    $this->__index_rel = array();
	    foreach ($this->__associations as $type) {
	        foreach ($this->$type as $assoc => $null) {
	            $this->__index_rel[$assoc] = $type;
	        }
	    }
	}

	function __setRel($relations, &$tables_index, $prefix)
	{
	    $sep = '.';

	    $assoc = $relations->tree;
	    $recursive = $relations->recursive;

	    $full_name = $prefix;

	    $tables_index[$full_name]['primary_key'] = array($this->primaryKey);

	    $this->recursive = $recursive;
	    $this->__indexedRel();
	    $this->__saveRel();
	    foreach ($assoc as $name => $next) {
	        $type = $this->__index_rel[$name];
	        $t =& $this->$type;
	        $t[$name] = $this->__backAssociation[$type][$name];
            $rel =& $t[$name];

    		$new_rel = new SQL_Relation();
    		$new_rel->tree = $next;
    		$new_rel->recursive = $recursive-1;

    		$tables_index[$full_name.$sep.$name]['obligatory'] = !empty($rel['obligatory']);
    		$tables_index[$full_name.$sep.$name]['many'] = SQL::$relation_types[$type] === 'many';

            $this->$name->{__FUNCTION__}($new_rel, $tables_index, $full_name.$sep.$name);
	    }
	}

	////////////////////////////////////////////////////////////////////////////

	function generateList($conditions = null, $order = null, $limit = null, $keyPath = null, $valuePath = null, $groupPath = null)
	{
		if ($keyPath == null && $valuePath == null && $groupPath == null && $this->hasField($this->displayField)) {
			$fields = array($this->primaryKey, $this->displayField);
		} else {
			$fields = null;
		}
		/*$recursive = $this->recursive;

		if ($groupPath == null && $recursive >= 1) {
			$this->recursive = -1;
		} elseif ($groupPath && $recursive >= 1) {
			$this->recursive = 0;
		}*/
		$result = $this->findAll($conditions, $fields, $order, $limit);
		/*$this->recursive = $recursive;*/

		if (!$result) {
			return false;
		}

		if ($keyPath == null) {
			$keyPath = '{n}.' . $this->name . '.' . $this->primaryKey;
		}

		if ($valuePath == null) {
			$valuePath = '{n}.' . $this->name . '.' . $this->displayField;
		}

		return Set::combine($result, $keyPath, $valuePath, $groupPath);
	}

	/**
	 * check field NULL property
	 *
	 * @param string $field
	 * @return boolean
	 */
	/*function isFieldNull($field)
	{
		foreach ($this->_tableInfo->value as $v)
		{
			if ($v['name'] == $field && $v['null'])
			{
				return true;
			}

		}
		return false;
	}

	function beforeSave()
	{
		foreach ($this->data[$this->name] as $k => $v)
		{
			if ($v === '' && $this->isFieldNull($k))
				$this->data[$this->name][$k] = null;
		}

		return true;
	}*/

	////////////////////////////////////////////////////////////////////////////

	/////  МЕТОДЫ ВАЛИДАЦИИ  ///////////////////////////////////////////////////

	function equalWith($data, $fieldName, $params)
    {
        return isset($this->data[$this->name][$fieldName]) && $data === $this->data[$this->name][$fieldName];
    }

    function customEreg($data, $regexp, $params)
    {
        if (mb_strpos($regexp, '^') === false) {
            $regexp = '.*'.$regexp;
        }
        if (mb_strpos($regexp, '$')+1 != mb_strlen($regexp)) {
            $regexp .= '.*';
        }
        mb_internal_encoding(DEFAULT_CHARSET);
        mb_regex_encoding(DEFAULT_CHARSET);
        return mb_ereg_match($regexp, $data);
    }

    function lengthBetween($data, $min, $max)
    {
        mb_internal_encoding(DEFAULT_CHARSET);
        $length = mb_strlen($data);
        return $length >= $min && $length <= $max;
    }

    function minLength($data, $value)
    {
        mb_internal_encoding(DEFAULT_CHARSET);
        return mb_strlen($data) >= $value;
    }

    function maxLength($data, $value)
    {
        mb_internal_encoding(DEFAULT_CHARSET);
        return mb_strlen($data) <= $value;
    }

    function enum($data, $list)
    {
        return in_array($data, $list);
    }

    function sqlDate($data)
    {
        return Date::valid_sql_date($data);
    }

    function sqlDateTime($data)
    {
        return Date::valid_sql_datetime($data);
    }

	////////////////////////////////////////////////////////////////////////////

	/////  ВАЛИДАЦИЯ  //////////////////////////////////////////////////////////

    protected $defaultRule = array (
		'allowEmpty'  => true,
		'required'    => false,
		'rule'        => null,
		'last'        => true,
		'on'          => null
    );

	function getErrorMessage($fieldName, $ruleName)
	{
	    $validator = $this->getValidator($fieldName, $ruleName);

	    $message = '';

		if (isset($validator['message'])) {
			$message = $validator['message'];
		} elseif (is_string($ruleName)) {
			$validator['message'] = $ruleName;
		} elseif (is_numeric($ruleName) && count($ruleName) > 1) {
		    $message = $ruleName + 1;
		} else {
			$message = __('This field cannot be left blank', true);
		}

		return $message;
	}

	function validateFieldByValidator($fieldName, $ruleName)
	{
		$data =& $this->getDataRef();
		$validator = $this->getValidator($fieldName, $ruleName);

	    $value =& $data[$fieldName];
		$Validation = new Validation();

		if (is_array($validator['rule'])) {
			$rule = $validator['rule'][0];
			unset($validator['rule'][0]);
			$ruleParams = am(array($data[$fieldName]), array_values($validator['rule']));
		} else {
			$rule = $validator['rule'];
			$ruleParams = array($data[$fieldName]);
		}

		$valid = true;

		if ($rule) {
    		if (method_exists($this, $rule) || isset($this->__behaviorMethods[$rule]) || isset($this->__behaviorMethods[low($rule)])) {
    			$ruleParams[] = array_diff_key($validator, $this->defaultRule);
    			$valid = call_user_func_array(array(&$this, $rule), $ruleParams);
    		} elseif (method_exists($Validation, $rule)) {
    			$valid = call_user_func_array(array(&$Validation, $rule), $ruleParams);
    		} elseif (!is_array($validator['rule'])) {
    			$valid = preg_match($rule, $data[$fieldName]);
    		}
		}
		return $valid;
	}

	function getValidator($fieldName, $ruleName)
	{
	    $ruleSet = $this->getRuleSet($fieldName);
        $validator = $ruleSet[$ruleName];
    	if (!is_array($validator)) {
    		$validator = array('rule' => $validator);
    	}

    	$validator = am($this->defaultRule, $validator);

    	return $validator;
	}

	function getRuleSet($fieldName)
	{
	    $ruleSet = $this->validate[$fieldName];
		if (!is_array($ruleSet) || (is_array($ruleSet) && isset($ruleSet['rule']))) {
			$ruleSet = array($ruleSet);
		}

		//$ruleSet = array_reverse($ruleSet, true);

        return $ruleSet;
	}

	function &getDataRef()
	{
		$data = array();
		if (isset($this->data[$this->name])) {
			$data =& $this->data[$this->name];
		} else {
			$data =& $this->data;
		}

		return $data;
	}

	function invalidFields($data = array())
	{
		if (!empty($this->behaviors)) {
			$behaviors = array_keys($this->behaviors);
			$ct = count($behaviors);
			for ($i = 0; $i < $ct; $i++) {
				if ($this->behaviors[$behaviors[$i]]->beforeValidate($this) === false) {
					return $this->validationErrors;
				}
			}
		}

		if (!$this->beforeValidate()) {
			return $this->validationErrors;
		}

		if (empty($data)) {
			$data = $this->getDataRef();
		} else {
			trigger_error(__('(Model::invalidFields) Parameter usage is deprecated, set the $data property instead', true), E_USER_WARNING);
		}

		if (empty($this->validate)) {
			return $this->validationErrors;
		}

		$exists = $this->exists();

		foreach ($this->validate as $fieldName => $ruleSet) {

		    $ruleSet = $this->getRuleSet($fieldName);
            if (array_key_exists('@dependent', $ruleSet)) {
                if (!$this->chackByDependent($ruleSet['@dependent'])) {
                    continue;
                } else {
                    unset($ruleSet['@dependent']);
                }
            }

			foreach ($ruleSet as $index => $validator) {

			    $validator = $this->getValidator($fieldName, $index);

				// Подходит ли текущее правило для данного случая
				if ((($validator['on'] == 'create') && $exists) || (($validator['on'] == 'update') && !$exists)) {
                    continue;
				}

				// В начале все хорошо, далее — череда проверок
			    $valid_flag = true;

                $stringType = true;
                $numberType = false;

                // Присуцтвует ли поле в наборе
                if (array_key_exists($fieldName, $data)) {
                    $value =& $data[$fieldName];
                    $empty = empty($data[$fieldName]) && (($numberType && ($value==='0')) || $stringType);
                    // Значение должно быть задано?
                    if ($empty) {
                        if ($validator['allowEmpty']) {
							break;
                        } else {
                            $valid_flag = false;
                        }
                    // Наконец-то проверка значения
                    } elseif (! $this->validateFieldByValidator($fieldName, $index)) {
                            $valid_flag = false;
                    }
			    } else {
                    // Поле должно быть?
                    if ($validator['required']) {
                        $valid_flag = false;
                    }
			    }

			    if (! $valid_flag) {
				    $this->invalidate($fieldName, $this->getErrorMessage($fieldName, $index));
				    if ($validator['last']) {
                        break;
				    }
			    }
			}
		}
		return $this->validationErrors;
	}

	function chackByDependent(&$dependent)
	{
	    $cond = true;

        $dependent = strval($dependent);

        $leadingFields = array();
        preg_match_all('/`([^`]+)`/s', $dependent, $matches, PREG_PATTERN_ORDER);
        if (isset($matches[1]) && count($matches[1])) {
            for ($i = 0; $i < count($matches[1]); $i++) {
                $leadingFields[] = $matches[1][$i];
            }
        }
        if ($leadingFields) {
            if (count(array_diff($leadingFields, array_keys($this->data[$this->name])))) {
                //throw new Exception();
                return $cond;
            }

            $knownLeadingFields = array();
            $unknownLeadingFields = array();
            $patterns = array();
            $replaces = array();

            foreach ($leadingFields as $fieldName) {
                $patterns[] = "/(`{$fieldName}`)/s";
                $replaces[] = "'".addslashes($this->data[$this->name][$fieldName])."'";
            }

            $expression = preg_replace($patterns, $replaces, $dependent);
        } else {
            $expression =& $dependent;
        }
        try {
            @eval('$cond = (bool)('.$expression.');');
        } catch (Exception $e) {}

        return $cond;
	}

    ////////////////////////////////////////////////////////////////////////////

    function afterFind($results, $primary = false)
	{
	    if (! method_exists($this, 'modifyRow')) {
	        return $results;
	    }
	    /**
	     * Избежание косвенной рекурсии.
	     */
	    static $calls = array();
	    $this_model_name = get_class($this);
	    if (!empty($calls[$this_model_name])) {
            return $results;
	    } else {
	        $calls[$this_model_name] = 0;
	    }
	    $calls[$this_model_name]++;



	    if (is_array($results)) {
            if ($primary) {
                if (isset($results[0][$this->name])) {
	                foreach ($results as &$row) {
    	                $this->modifyRow($row[$this->name], $primary);
	                }
                }
    	    } else {
	            if (isset($results[0][$this->name][0])) {
	                foreach ($results[0][$this->name] as &$row) {
    	                $this->modifyRow($row, $primary);
	                }
	            } elseif (isset($results[0][$this->name])) {
                    $this->modifyRow($results[0][$this->name], $primary);
	            } elseif (isset($results[0][$this->primaryKey])) {
        	        foreach ($results as &$row) {
    	                $this->modifyRow($row, $primary);
    	            }
	            } elseif (isset($results[$this->primaryKey])) {
	                $this->modifyRow($results, $primary);
	            } elseif (isset($results[0]) && (count($results) == 1) && (count($results[0]) == 1)) {
	                reset($results[0]);
	                $relation_name = key($results[0]);
        	        foreach ($results[0][$relation_name] as &$row) {
    	                $this->modifyRow($row, $primary);
    	            }
	            }
    	    }
	    }

	    /**
	     * Избежание косвенной рекурсии.
	     */
	    $calls[$this_model_name]--;

		return $results;
	}

	function find()
	{
	    $args =& func_get_args();

		/**
		 * Модифицируем условия, если необходимо.
		 */
	    if (method_exists($this, 'modifyConditions')) {

    		/**
    		 * Создаём ссылку $conditions на элемент списка аргументов, содержащий условия.
    		 */
	        if (!count($args)) {
                $args[0] = array();
	        }
            $conditions =& $args[0];
	        if (!(!is_string($conditions) || (is_string($conditions) && !array_key_exists($conditions, $this->__findMethods)))) {
    		    if (empty($args[1]['conditions'])) {
        		    $args[1]['conditions'] = array();
        		}
                $conditions =& $args[1]['conditions'];
    		}

    		/**
    		 * Всегда будем работать с условиями как с массивом.
    		 */
    		if (empty($conditions)) {
    		    $conditions = array();
    		}
    		if (! is_array($conditions)) {
	            $_cond = $conditions;
	            $conditions = array($_cond);
	        }

    		/**
    		 * Модифицируем условия.
    		 */
	        $this->modifyConditions($conditions);
        }

	    $result =& call_user_func_array(array('parent', __FUNCTION__), $args);
	    return $result;
	}

    /**
     * Возвращает array_assoc изменённых, относительно сохранённых, полей с текущими значениями (т. е. из БД).
     *
     * @param array $changed_fields Список проверяемых полей. Необязательно.
     *
     * @return array asoc
     */
    function getChangedFields($changed_fields = array())
    {
        $from_list = count($changed_fields);
        if ($from_list) {
            $changed_fields = array_combine($changed_fields, $changed_fields);
        } else {
            $changed_fields = array();
        }
	    if ($this->exists()) {
            $clone_model = clone $this;
            $clone_model->rel();
            $old_data = $clone_model->read($from_list ? array_keys($changed_fields) : null);
            if (empty($old_data[$this->name])) {
                throw new DBException();
            }

            foreach ($old_data[$this->name] as $field_name => $old_value) {
                if (array_key_exists($field_name, $this->data[$this->name]) &&
                    $this->data[$this->name][$field_name] !== $old_value)
                {
                    $changed_fields[$field_name] = $old_value;
                } else {
                    if (array_key_exists($field_name, $changed_fields)) {
                        unset($changed_fields[$field_name]);
                    }
                }
            }
        }

        return $changed_fields;
    }

    /**
	 * Вызов событий beforeChange() и afterChange() для определённых в модели полей.
	 * Для обработки события нужно создать метод с именем вида:
	 * beforeChange_<название_поля>() или соответственно afterChange_<название_поля>().
	 *
	 * Событие beforeChange является cancellable, afterChange — нет.
	 * @todo Следует учесть, что на пути от начала вызовов событий до конца данные могут меняться.
	 */
    ///////////////////////////////////////////////////////////////////////
    protected $_on_after_change_fields = array();

    protected function _throw_beforeChange()
    {
        $method_pref = 'beforeChange_';

        $this->_on_after_change_fields = array();

        if (! $this->exists()) {
            return true;
        }

        $after_change_observable = $this->_getOnAfterChangeObservableFields();
        if (count($after_change_observable)) {
            $this->_on_after_change_fields = $this->getChangedFields($after_change_observable);
        }

        $changed_fields = array();
        foreach ($this->_getOnBeforeChangeObservableFields() as $field_name) {
            if (array_key_exists($field_name, $this->data[$this->name])) {
                $changed_fields[] = $field_name;
            }
        }
        if (count($changed_fields)) {
            foreach ($this->getChangedFields($changed_fields) as $field_name => $old_value) {
                $method_name = $method_pref.$field_name;
                if (! $this->{$method_name}($this->data[$this->name][$field_name], $old_value)) {
                    return false;
                }
            }
        }

        return true;
    }

    protected function _throw_afterChange()
    {
        $method_pref = 'afterChange_';

        foreach ($this->_on_after_change_fields as $field_name => $old_value) {
            /**
             * Вдруг была отмена?..
             */
            if (! array_key_exists($field_name, $this->data[$this->name])) {
                continue;
            }
            $method_name = $method_pref.$field_name;

            $to_dont_change_from_method = $this->data[$this->name][$field_name];
            $this->{$method_name}($to_dont_change_from_method, $old_value);
        }
    }

    protected function _getOnBeforeChangeObservableFields()
    {
        $method_pref = 'beforeChange_';
        $observable_fields = array();
        foreach (get_class_methods($this) as $method_name) {
            if ((strpos($method_name, $method_pref) === 0) && (strlen($method_name) > strlen($method_pref))) {
                $observable_fields[] = substr($method_name, strlen($method_pref));
            }
        }

        return $observable_fields;
    }

    protected function _getOnAfterChangeObservableFields()
    {
        $method_pref = 'afterChange_';
        $observable_fields = array();
        foreach (get_class_methods($this) as $method_name) {
            if ((strpos($method_name, $method_pref) === 0) && (strlen($method_name) > strlen($method_pref))) {
                $observable_fields[] = substr($method_name, strlen($method_pref));
            }
        }

        return $observable_fields;
    }

    function beforeSave()
	{
	    if (! $this->_throw_beforeChange()) {
            return false;
	    }

		return parent::beforeSave();
	}

    function afterSave()
	{
	    $this->_throw_afterChange();
	}
    ///////////////////////////////////////////////////////////////////////

    protected function _buildFieldForOrder($fieldName, $list)
	{
	    if (!is_array($list) || empty($list)) {
	        return '';
	    }
	    $head = '';
        reset($list);
        list($k, $v) = each($list);
        $k = str($k)->replace("'", "\\'")->toString();
        $v = str($v)->replace("'", "\\'")->toString();
	    $head = "{$fieldName} = '{$k}', '{$v}', ";

	    $tail = '';
	    if (count($list) > 1) {
	        array_shift($list);
	        $tail = $this->{__FUNCTION__}($fieldName, $list);
	    } else {
	        $tail = 'NULL';
	    }

	    return "IF({$head}{$tail})";
	}

	/**
	 * Строит поле $alias для сортировки.
	 * Необходимо для правильной сортировки полей типа ENUM, отображаемых посредством ассоциативного массива.
	 *
	 * @param string $fieldName
	 * @param string $alias
	 * @param array_assoc $list
	 * @return string
	 */
    function buildFieldForOrder($fieldName, $alias, $list)
	{
	    if (empty($list)) {
	        return '';
	    }

	    return $this->_buildFieldForOrder($fieldName, $list)." AS {$alias}";
	}

	/**
	 * В данной реализации, если все поля присуцтвуют в $this->data, то они не обновляются с БД.
	 *
	 * @param array[optional] $fields_list
	 */
	function getStoredFields($fields_list = null)
	{
	    $all_fields = array_keys($this->schema()->value);
	    $needle_fields = isset($fields_list) ? array_intersect($fields_list, $all_fields) : $all_fields;
	    $have_data = isset($this->data[$this->name]) ? $this->data[$this->name] : array();
	    $exists_fields = array_intersect(array_keys($have_data), $all_fields);
	    $data = array_combine($needle_fields, array_fill(0, count($needle_fields), null));
	    if (count(array_diff($needle_fields, $exists_fields))) {
            if (empty($this->id)) {
                return null; // Или исключение?
            }
	        $this->__systemQuery = true;
            $this->rel();
            $tmp = $this->read(null, $this->id);
            if (isset($tmp[$this->name]) && is_array($tmp[$this->name])) {
                $have_data = $tmp[$this->name];
            }
	        $this->__systemQuery = false;
	    }
        foreach ($data as $field_name => &$value) {
            $value = $have_data[$field_name];
        }

	    return $data;
	}
}

?>
